package com.senriot.quickup.service

import com.querydsl.core.BooleanBuilder
import com.querydsl.core.types.Predicate
import com.senriot.cloud.common.AbstractService
import com.senriot.cloud.common.error.BadRequestAlertException
import com.senriot.cloud.common.error.EmailAlreadyUsedException
import com.senriot.cloud.common.error.InvalidPasswordException
import com.senriot.cloud.common.error.LoginAlreadyUsedException
import com.senriot.cloud.common.result.R
import com.senriot.cloud.common.result.fail
import com.senriot.cloud.common.result.success
import com.senriot.quickup.config.ANONYMOUS_USER
import com.senriot.quickup.config.DEFAULT_LANGUAGE
import com.senriot.quickup.domain.QAgency
import com.senriot.quickup.domain.QUser
import com.senriot.quickup.domain.User
import com.senriot.quickup.repository.AgencyRepository
import com.senriot.quickup.repository.RoleRepository
import com.senriot.quickup.repository.UserRepository
import com.senriot.quickup.security.*
import com.senriot.quickup.service.dto.StaffDTO
import com.senriot.quickup.service.dto.UserDTO
import com.senriot.quickup.service.mapper.UserMapper
import com.senriot.quickup.service.util.generateActivationKey
import com.senriot.quickup.service.util.generateResetKey
import org.slf4j.LoggerFactory
import org.springframework.cache.CacheManager
import org.springframework.data.domain.Page
import org.springframework.data.domain.Pageable
import org.springframework.security.core.userdetails.UsernameNotFoundException
import org.springframework.security.crypto.password.PasswordEncoder
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import java.time.Instant
import java.util.*

/**
 * Service class for managing users.
 */
@Service
@Transactional
class UserService(
        private val passwordEncoder: PasswordEncoder,
        private val authorityRepository: RoleRepository,
        private val cacheManager: CacheManager,
        private val agencyRepository: AgencyRepository
) : AbstractService<User, Long, UserDTO, UserRepository, UserMapper>()
{

    private val log = LoggerFactory.getLogger(javaClass)

    fun activateRegistration(key: String): Optional<User>
    {
        log.debug("Activating user for activation key {}", key)
        return repository.findOneByActivationKey(key)
                .map { user ->
                    user.activated = true
                    user.activationKey = null
                    log.debug("Activated user: {}", user)
                    user
                }
    }

    fun completePasswordReset(newPassword: String, key: String): Optional<User>
    {
        log.debug("Reset user password for reset key {}", key)
        return repository.findOneByResetKey(key)
                .filter { user -> user.resetDate?.isAfter(Instant.now().minusSeconds(86400)) ?: false }
                .map { user ->
                    user.password = passwordEncoder.encode(newPassword)
                    user.resetKey = null
                    user.resetDate = null
                    user
                }
    }

    fun requestPasswordReset(mail: String): Optional<User>
    {
        return repository.findOneByEmailIgnoreCase(mail)
                .filter(User::activated)
                .map { user ->
                    user.resetKey = generateResetKey()
                    user.resetDate = Instant.now()
                    user
                }
    }

    fun restPassword(oldPassword: String, newPassword: String)
    {
        getCurrentUserLogin().map { login ->
            repository.findOneByLogin(login).map {
                if (passwordEncoder.matches(oldPassword, it.password))
                {
                    it.password = passwordEncoder.encode(newPassword)
                    repository.saveAndFlush(it)
                } else
                {
                    throw BadRequestAlertException("原密码不正确", "user", "password_error")
                }
            }.orElseThrow { UsernameNotFoundException("用户不存在") }
        }.orElseThrow { UsernameNotFoundException("用户不存在") }
    }

    fun registerUser(userDTO: UserDTO, password: String): User
    {
        val login = userDTO.login ?: throw IllegalArgumentException("Empty login not allowed")
        val email = userDTO.email ?: throw IllegalArgumentException("Empty email not allowed")
        repository.findOneByLogin(login.toLowerCase()).ifPresent { existingUser ->
            val removed = removeNonActivatedUser(existingUser)
            if (!removed)
            {
                throw LoginAlreadyUsedException()
            }
        }
        repository.findOneByEmailIgnoreCase(email).ifPresent { existingUser ->
            val removed = removeNonActivatedUser(existingUser)
            if (!removed)
            {
                throw EmailAlreadyUsedException()
            }
        }
        val newUser = mapper.toEntity(userDTO)
        val encryptedPassword = passwordEncoder.encode(password)
        newUser.apply {
            this.login = login.toLowerCase()
            this.password = encryptedPassword
            this.email = email.toLowerCase()
            activated = false
            activationKey = generateActivationKey()
            authorities = mutableSetOf()
            authorityRepository.findByName(com.senriot.quickup.security.USER).ifPresent { authorities.add(it) }
        }
        repository.save(newUser)
        log.debug("Created Information for User: {}", newUser)
        return newUser
    }

    private fun removeNonActivatedUser(existingUser: User): Boolean
    {
        if (existingUser.activated)
        {
            return false
        }
        repository.delete(existingUser)
        repository.flush()
        return true
    }

    fun createUser(userDTO: UserDTO): User
    {
        val encryptedPassword = passwordEncoder.encode(userDTO.login)
        var user = mapper.toEntity(userDTO).apply {
            login = userDTO.login?.toLowerCase()
            email = userDTO.email?.toLowerCase()
            langKey = userDTO.langKey ?: DEFAULT_LANGUAGE // default language
            password = encryptedPassword
            resetKey = generateResetKey()
            resetDate = Instant.now()
            activated = true
        }
//        userDTO.authorities?.apply {
//            val authorities = this.asSequence()
//                    .map(authorityRepository::findByName)
//                    .filter(Optional<Role>::isPresent)
//                    .mapTo(mutableSetOf()) { it.get() }
//            user.authorities = authorities
//        }
        user = repository.save(user)
        log.debug("Created Information for User: {}", user)
        return user
    }


    /**
     * Update all information for a specific user, and return the modified user.
     *
     * @param userDTO user to update.
     * @return updated user.
     */
    fun updateUser(userDTO: UserDTO): Optional<UserDTO>
    {
        return Optional.of(repository.findById(userDTO.id!!))
                .filter(Optional<User>::isPresent)
                .map { it.get() }
                .map { user ->
                    mapper.updateEntity(userDTO, user)
                    user.apply {
                        login = userDTO.login!!.toLowerCase()
                        email = userDTO.email?.toLowerCase()
                    }
                    val managedAuthorities = user.authorities
                    managedAuthorities.clear()
                    userDTO.authorities?.apply {
                        this.asSequence()
                                .map { authorityRepository.findById(it) }
                                .filter { it.isPresent }
                                .mapTo(managedAuthorities) { it.get() }
                    }
                    log.debug("Changed Information for User: {}", user)
                    user
                }
                .map { mapper.toDto(it) }
    }

    fun deleteUser(login: String)
    {
        repository.findOneByLogin(login).ifPresent { user ->
            repository.delete(user)
            log.debug("Deleted User: {}", user)
        }
    }

    fun changePassword(currentClearTextPassword: String, newPassword: String)
    {
        getCurrentUserLogin()
                .flatMap(repository::findOneByLogin)
                .ifPresent { user ->
                    val currentEncryptedPassword = user.password
                    if (!passwordEncoder.matches(currentClearTextPassword, currentEncryptedPassword))
                    {
                        throw InvalidPasswordException()
                    }
                    val encryptedPassword = passwordEncoder.encode(newPassword)
                    user.password = encryptedPassword
                    log.debug("Changed password for User: {}", user)
                }
    }

    @Transactional(readOnly = true)
    fun getAllManagedUsers(pageable: Pageable): Page<UserDTO> =
            repository.findAllByLoginNot(pageable, ANONYMOUS_USER).map { mapper.toDto(it) }

    @Transactional(readOnly = true)
    fun getUserWithAuthoritiesByLogin(login: String): Optional<User> =
            repository.findOneWithAuthoritiesByLogin(login)

    @Suppress("unused")
    @Transactional(readOnly = true)
    fun getUserWithAuthorities(id: Long): Optional<User> =
            repository.findOneWithAuthoritiesById(id)

    @Transactional(readOnly = true)
    fun getUserWithAuthorities(): Optional<User> =
            getCurrentUserLogin().flatMap(repository::findOneWithAuthoritiesByLogin)

    /**
     * Not activated users should be automatically deleted after 3 days.
     *
     * This is scheduled to get fired everyday, at 01:00 (am).
     */
//    @Scheduled(cron = "0 0 1 * * ?")
//    fun removeNotActivatedUsers()
//    {
//        repository
//                .findAllByActivatedIsFalseAndActivationKeyIsNotNullAndCreatedDateBefore(
//                        Instant.now().minus(3, ChronoUnit.DAYS)
//                )
//                .forEach { user ->
//                    log.debug("Deleting not activated user {}", user.login)
//                    repository.delete(user)
//                    clearUserCaches(user)
//                }
//    }

    /**
     * @return a list of all the authorities
     */
    fun getAuthorities() = authorityRepository.findAll().asSequence().map { it.name }.filterNotNullTo(mutableListOf())

    /**
     * 获取人员
     * @param page Pageable
     * @param predicate Predicate?
     * @return Page<StaffDTO>
     */
    fun getStaffs(page: Pageable,
                  predicate: Predicate?,
                  deviceId: String?): Page<StaffDTO>
    {
        val user = QUser.user
        val p = BooleanBuilder()
        predicate?.let { p.and(it) }
        deviceId?.let { p.and(user.bonuses.isEmpty.or(user.bonuses.any().device.id.notIn(it))) }
        p.and(user.authorities.any().name.eq("ROLE_RESELLER"))
        if (!isCurrentUserInRole(ADMIN))
        {
            getCurrentUserLogin().map { login ->
                agencyRepository.findOne(QAgency.agency.account.login.eq(login)).map {
                    p.and(user.agency.eq(it))
                }.orElseThrow {
                    BadRequestAlertException("无操作权限", "user", "usernull")
                }
            }.orElseThrow {
                BadRequestAlertException("无操作权限", "user", "usernull")
            }
        }

        return repository.findAll(p, page).map { mapper.toStaffDTO(it) }
    }

    fun updateStatus(id: Long, status: Int): Optional<StaffDTO>?
    {
        return repository.findById(id).map {
            it.status = status
            mapper.toStaffDTO(repository.save(it))
        }
    }

    fun requestLink(): R<String>
    {
        return when
        {
            isCurrentUserInRole(ADMIN)                                               -> success("${WX_DOMAIN}reseller/signup")
            isCurrentUserInRole(AGENCY_PROVINCE) || isCurrentUserInRole(AGENCY_CITY) -> repository.findOneByLogin(
                    getCurrentUserLogin().get()).map {
                success("${WX_DOMAIN}reseller/signup?agencyId=${it.agency?.id}")
            }.orElseGet { fail("用户没找到") }
            else                                                                     -> fail("权限错误")
        }
    }
}
